﻿//---------------------------------------------------------------------
//Copyright (c) 2009, Pierre Milet
//All rights reserved.
//
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
//INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
//OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
//TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using Microsoft.BizTalk.Message.Interop;
using System.Linq;
using System.Xml.XPath;
using Microsoft.BizTalk.Streaming;

namespace BizMock
{
    
    public class MessageFileHelper
    {
        public static void SaveMessage(Stream msgBody, string destinationFolder, string portName, string messageId, string ctxFileNamePrefix, string msgFileNamePrefix, string ctrlFileNamePrefix)
        {
            var partInfos = new List<MessagePartInfo>();
            var messagePartInfo = new MessagePartInfo
            {
                Id = messageId,
                Name = "Body",
                FileName = String.Format("{0}_{1}_{2}_part{3}.out", portName, msgFileNamePrefix, messageId, 0),
                Charset = "UTF-8", 
                ContentType = "text/xml", 
                IsBodyPart = true
            };
            partInfos.Add(messagePartInfo);

            var ctrlFileName = string.Format("{0}_{1}_{2}.xml", portName, ctrlFileNamePrefix, messageId);
            var contextFileName = string.Format("{0}_{1}_{2}.xml", portName, ctxFileNamePrefix, messageId);
            var bodyPartFileName = partInfos[0].FileName;
            SaveMessageContextFile(new NullContext(), partInfos, destinationFolder, contextFileName);
            SaveMessagePart(msgBody, partInfos[0], destinationFolder);
            SaveControlFile(destinationFolder, contextFileName, bodyPartFileName, ctrlFileName);
        }

        public static void SaveMessage(IBaseMessage message, string destinationFolder, string portName, string messageId, string ctxFileNamePrefix, string msgFileNamePrefix, string ctrlFileNamePrefix)
        {
            var partInfos = new List<MessagePartInfo>();

            for (var i = 0; i < message.PartCount; i++)
            {
                string partName;
                IBaseMessagePart part = message.GetPartByIndex(i, out partName);

                var messagePartInfo = new MessagePartInfo
                {
                    Id = part.PartID.ToString(),
                    Name = partName,
                    FileName = String.Format("{0}_{1}_{2}_part{3}.out", portName, msgFileNamePrefix, messageId, i),
                    Charset = part.Charset,
                    ContentType = part.ContentType,
                    IsBodyPart = partName == message.BodyPartName
                };
                partInfos.Add(messagePartInfo);
            }

            var ctrlFileName = string.Format("{0}_{1}_{2}.xml", portName, ctrlFileNamePrefix, messageId);
            var contextFileName = string.Format("{0}_{1}_{2}.xml", portName, ctxFileNamePrefix, messageId);
            var bodyPartFileName = (from p in partInfos
                                    where p.IsBodyPart
                                    select p.FileName).FirstOrDefault();
            SaveMessageContextFile(message.Context, partInfos, destinationFolder, contextFileName);

            for (var i = 0; i < message.PartCount; i++)
            {
                string partName;
                IBaseMessagePart part = message.GetPartByIndex(i, out partName);
                var messagePartInfo = partInfos;
                SaveMessagePart(part.Data, messagePartInfo[i], destinationFolder);
            }

            SaveControlFile(destinationFolder, contextFileName, bodyPartFileName, ctrlFileName);
        }

        private static void SaveControlFile(string destinationFolder, string contextFileName, string bodyPartFileName, string ctrlFileName)
        {
            string ctxFilePath = string.Format("{0}\\{1}", destinationFolder, contextFileName);
            string ctrlFilePath = string.Format("{0}\\{1}", destinationFolder, ctrlFileName);
            string bodyFilePath = string.Format("{0}\\{1}", destinationFolder, bodyPartFileName);

            using (var xw = XmlWriter.Create(ctrlFilePath))
            {
                if (xw == null)
                    throw new NullReferenceException("Failed to create XmlWriter");

                xw.WriteStartDocument();
                xw.WriteStartElement("MessageLocation");
                xw.WriteAttributeString("ContextFile", ctxFilePath);
                xw.WriteString(bodyFilePath);
                xw.WriteEndElement();
                xw.Close();
            }
        }

        private static void SaveMessageContextFile(IBaseMessageContext context, IEnumerable<MessagePartInfo> messagePartInfos, string destinationFolder, string contextFileName)
        {
            var contextFileFullPath = Path.Combine(destinationFolder, contextFileName);

            var xmlWriterSettings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "\t",
                OmitXmlDeclaration = false,
                Encoding = Encoding.UTF8
            };

            using (var xw = XmlWriter.Create(contextFileFullPath, xmlWriterSettings))
            {
                if (xw == null)
                    throw new NullReferenceException("Failed to create XmlWriter");

                xw.WriteStartDocument();

                // <MessageInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                xw.WriteStartElement("MessageInfo");
                xw.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
                xw.WriteAttributeString("xmlns", "xsd", null, "http://www.w3.org/2001/XMLSchema");

                //      <ContextInfo PropertiesCount="...">
                xw.WriteStartElement("ContextInfo");
                xw.WriteAttributeString("PropertiesCount", context.CountProperties.ToString());

                for (var i = 0; i < context.CountProperties; i++)
                {
                    string propName;
                    string propNamespace;
                    var propValue = context.ReadAt(i, out propName, out propNamespace);
                    var isPromoted = context.IsPromoted(propName, propNamespace);

                    //      <Property Name="..." Namespace="..." Value="..." Promoted="...">
                    xw.WriteStartElement("Property");
                    xw.WriteAttributeString("Name", propName);
                    xw.WriteAttributeString("Namespace", propNamespace);
                    xw.WriteAttributeString("Value", propValue.ToString());
                    xw.WriteAttributeString("Promoted", isPromoted.ToString().ToLowerInvariant());
                    xw.WriteEndElement();
                    //      </Property>
                }

                //          <ArrayProperty Name="PartNames" Namespace="http://schemas.microsoft.com/BizTalk/2003/messageagent-properties">
                xw.WriteStartElement("ArrayProperty");
                xw.WriteAttributeString("Name", "PartNames");
                xw.WriteAttributeString("Namespace", "http://schemas.microsoft.com/BizTalk/2003/messageagent-properties");

                foreach (var messagePartInfo in messagePartInfos)
                {
                    //          <ArrayElement1 Value="{partName}">
                    xw.WriteStartElement("ArrayElement1");
                    xw.WriteAttributeString("Value", messagePartInfo.Name);
                    xw.WriteEndElement();
                    //          </ArrayElement1>
                }
                xw.WriteEndElement();
                //          </ArrayProperty>

                xw.WriteEndElement();
                //      </ContextInfo>

                //      <PartInfo PartsCount="...">
                xw.WriteStartElement("PartInfo");

                foreach (var messagePartInfo in messagePartInfos)
                {
                    //          <MessagePart ID="..." Name="..." FileName="..." Charset="..." ContentType="..." /> 
                    xw.WriteStartElement("MessagePart");
                    xw.WriteAttributeString("ID", messagePartInfo.Id);
                    xw.WriteAttributeString("Name", messagePartInfo.Name);
                    xw.WriteAttributeString("FileName", Path.Combine(destinationFolder, messagePartInfo.FileName));
                    xw.WriteAttributeString("Charset", messagePartInfo.Charset);
                    xw.WriteAttributeString("ContentType", messagePartInfo.ContentType);
                    xw.WriteAttributeString("IsBodyPart", messagePartInfo.IsBodyPart.ToString().ToLowerInvariant());
                    xw.WriteEndElement();
                    //          </MessagePart>
                }

                xw.WriteEndElement();
                //      </PartInfo>

                xw.WriteEndElement();
                // </MessageInfo>

                xw.Close();
            }
        }

        private static void SaveMessagePart(Stream messagePartDataStream, MessagePartInfo messagePartInfo, string destinationFolder)
        {
            var partFileFullPath = Path.Combine(destinationFolder, messagePartInfo.FileName);

            messagePartDataStream.Position = 0;
            using (var fileStream = new FileStream(partFileFullPath, FileMode.CreateNew))
            {
                int bytesRead;
                var buff = new byte[8 * 1024];
                while ((bytesRead = messagePartDataStream.Read(buff, 0, buff.Length)) > 0)
                    fileStream.Write(buff, 0, bytesRead);
                fileStream.Close();
            }
        }

        public static IBaseMessage LoadFromBodyContentsFile(string bodyFileFullPath, IBaseMessageFactory messageFactory)
        {
            IBaseMessage msg = messageFactory.CreateMessage();
            msg.Context = messageFactory.CreateMessageContext();

            IBaseMessagePart bodyPart = messageFactory.CreateMessagePart();
            bodyPart.Data = LoadPartContentsToVirtualStream(bodyFileFullPath);
            msg.AddPart("Body", bodyPart, true);

            return msg;
        }

        public static IBaseMessage LoadFromBodyContentsString(string message, IBaseMessageFactory messageFactory )
        {
             IBaseMessage msg = messageFactory.CreateMessage();
            msg.Context = messageFactory.CreateMessageContext();

            IBaseMessagePart bodyPart = messageFactory.CreateMessagePart();
            byte[] bytes = System.Text.ASCIIEncoding.ASCII.GetBytes(message);
            MemoryStream ms = new MemoryStream(bytes);
            ms.Position = 0;
            bodyPart.Data = ms;
            msg.AddPart("Body", bodyPart, true);

            return msg;
        }
                
        public static IBaseMessagePart LoadFromPartContentsFile( MessagePartInfo msgPartInfo, IBaseMessageFactory messageFactory)
        {
            IBaseMessagePart part = messageFactory.CreateMessagePart();
            part.ContentType = msgPartInfo.ContentType;
            part.Charset = msgPartInfo.Charset;
            part.Data = msgPartInfo.Data;

            return part;
        }

        public static IBaseMessage LoadMessage(string ctrlFileFullPath, IBaseMessageFactory messageFactory)
        {
            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(ctrlFileFullPath);

            IBaseMessage msg = messageFactory.CreateMessage();
            IBaseMessageContext context = messageFactory.CreateMessageContext();
            msg.Context = context;

            var nodeCtx = xdoc.SelectSingleNode("MessageLocation/@ContextFile");
            if (nodeCtx != null)
            {
                string contextFileFullPath = xdoc.SelectSingleNode("MessageLocation/@ContextFile").InnerText;

                XPathNavigator navDocument = new XPathDocument(contextFileFullPath).CreateNavigator();
                XPathNodeIterator iterProperty = navDocument.Select("//Property");
                foreach (XPathNavigator navProperty in iterProperty)
                {
                    string propName = navProperty.GetAttribute("Name", "");
                    string propNamespace = navProperty.GetAttribute("Namespace", "");
                    string propValue = navProperty.GetAttribute("Value", "");
                    bool propIsPromoted = XmlConvert.ToBoolean(navProperty.GetAttribute("Promoted", ""));
                    if (propIsPromoted)
                    {
                        context.Promote(propName, propNamespace, propValue);
                    }
                    else
                    {
                        context.Write(propName, propNamespace, propValue);
                    }
                }
                XPathNodeIterator iterMessagePart = navDocument.Select("//MessagePart");
                foreach (XPathNavigator navMessagePart in iterMessagePart)
                {
                    LoadPart(msg, navMessagePart, messageFactory, contextFileFullPath);
                }
            }
            else
            {
                // old format, without Context File
                string msgBodyFileFullPath = xdoc.SelectSingleNode("MessageLocation").InnerText;

                IBaseMessagePart part = messageFactory.CreateMessagePart();
                part.Data = LoadPartContentsToVirtualStream(msgBodyFileFullPath);

                msg.AddPart("Body", part, true);
            }
            return msg;
        }

        private static void LoadPart(IBaseMessage msg, XPathNavigator node, IBaseMessageFactory messageFactory, String contextFileFullPath)
        {
            string partName = node.GetAttribute("Name", "");
            string partFileName = node.GetAttribute("FileName", "");
            string strIsBodyPart = node.GetAttribute("IsBodyPart", "");
            bool isBodyPart;
            if (!String.IsNullOrEmpty(strIsBodyPart))
            {
                isBodyPart = XmlConvert.ToBoolean(strIsBodyPart);
            }
            else
            {
                isBodyPart = true;
            }
            XmlResolver resolver = new XmlUrlResolver();
            IBaseMessagePart part = messageFactory.CreateMessagePart();

            // NOTE: 
            // Copy the network stream into a virtual stream stream. If we were
            // to use a forward only stream (as in the response stream) we would 
            // not be able to suspend the response data on failure. The virtual 
            // stream will overflow to disc when it reaches a given threshold
            string partFullPath = resolver.ResolveUri(new Uri(contextFileFullPath), partFileName).LocalPath;
            part.Data = LoadPartContentsToVirtualStream(partFullPath);
            part.Charset = node.GetAttribute("Charset", "");
            part.ContentType = node.GetAttribute("ContentType", "");

            msg.AddPart(partName, part, isBodyPart);
        }

        private static VirtualStream LoadPartContentsToVirtualStream(string partContentsFullPath)
        {
            var vs = new VirtualStream();

            using (var s = File.OpenRead(partContentsFullPath))
            {
                int bytesRead = 0;
                byte[] buff = new byte[8 * 1024];
                while ((bytesRead = s.Read(buff, 0, buff.Length)) > 0)
                    vs.Write(buff, 0, bytesRead);
            }
            vs.Position = 0;

            return vs;
        }

        public static void RemoveBizMockMessage(string ctrlFile)
        {
            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(ctrlFile);

            var nodeCtx = xdoc.SelectSingleNode("MessageLocation/@ContextFile");
            if (nodeCtx != null)
            {
                string ctxFile = nodeCtx.InnerText;

                XmlDocument xdocCtx = new XmlDocument();
                xdocCtx.Load(ctxFile);

                var partFileNames = from XmlNode fn
                                    in xdocCtx.SelectNodes("/MessageInfo/PartInfo/MessagePart/@FileName")
                                    select fn.InnerText;

                foreach (var partFileName in partFileNames)
                {
                    File.Delete(partFileName);
                }

                File.Delete(ctxFile);
                File.Delete(ctrlFile);
            }
            else
            {
                // old format, without Context File
                string msgBodyFile = xdoc.SelectSingleNode("MessageLocation").InnerText;
                File.Delete(msgBodyFile);
            }
        }

        public class MessagePartInfo
        {
            
            private Stream _data;

            public string Id { get; set; }
            public string Name { get; set; }
            public string FileName { get; set; }
            public string Charset { get; set; }
            public string ContentType { get; set; }
            public bool IsBodyPart { get; set; }
            public Stream Data
            {
                get
                {
                    if( _data == null )
                        _data = LoadPartContentsToVirtualStream(FileName);
                    return _data;
                }
                set
                {
                    _data = value;
                }
            }
        }

        internal class NullContext : IBaseMessageContext
        {
            public void AddPredicate(string strName, string strNameSpace, object obj)
            {
                throw new NotImplementedException();
            }

            public uint CountProperties
            {
                get { return 0; }
            }

            public ContextPropertyType GetPropertyType(string strName, string strNameSpace)
            {
                throw new NotImplementedException();
            }

            public bool IsPromoted(string strName, string strNameSpace)
            {
                return false;
            }

            public void Promote(string strName, string strNameSpace, object obj)
            {
                throw new NotImplementedException();
            }

            public object Read(string strName, string strNamespace)
            {
                return null;
            }

            public object ReadAt(int index, out string strName, out string strNamespace)
            {
                throw new NotImplementedException();
            }

            public void Write(string strName, string strNameSpace, object obj)
            {
                throw new NotImplementedException();
            }
        }
        
    }
}
